import {
    Body,
    Controller,
    Get,
    Headers,
    InternalServerErrorException,
    MaxFileSizeValidator,
    NotFoundException,
    Param,
    ParseFilePipe,
    Post,
    Query,
    Req,
    Res,
    StreamableFile,
    UploadedFiles,
    UseInterceptors
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { AnyFilesInterceptor } from "@nestjs/platform-express";
import { ApiBearerAuth, ApiConsumes, ApiHeader, ApiOperation, ApiParam, ApiQuery, ApiTags } from "@nestjs/swagger";
import { SkipThrottle, Throttle } from "@nestjs/throttler";
import { Request, Response } from "express";
import { createReadStream, existsSync, statSync } from "fs";
import { join } from "path";

import { SkipJwtAuth } from "@/core/auth/decorator/skip-auth.decorator";
import { CONFIG_KEY } from "@/core/config/configurations";
import { MIMETypes } from "@/core/constant/global-enum";
import { ApiBaseResponse } from "@/core/decorators/api-base-response.decorator";
import AjaxResult from "@/core/utils/AjaxResult";

import { CreateFileDto } from "../dto/create-file.dto";
import { FileService } from "../service/file.service";
import { UploadFileVO } from "../vo/upload-file.vo";

@ApiTags("文件上传下载")
@ApiBearerAuth()
@Controller("/storage")
export class StorageController {
    constructor(
        private readonly configService: ConfigService,
        private readonly fileService: FileService
    ) {}

    @ApiOperation({ summary: "上传文件", description: "单文件上传，无类型和字段限制" })
    @ApiBaseResponse({ type: "array", items: UploadFileVO })
    @ApiConsumes("multipart/form-data")
    @Throttle({ default: { limit: 20, ttl: 1000 } }) // 覆盖全局限流配置，放宽上传频率限制
    // @UseInterceptors(FileInterceptor("files", { limits: { files: 10 } }))
    @UseInterceptors(AnyFilesInterceptor({ limits: { files: 1 } }))
    @Post("/upload")
    async upload(
        @UploadedFiles(
            new ParseFilePipe({
                validators: [
                    new MaxFileSizeValidator({ maxSize: 1024 * 1024 * 100, message: "文件大小不能超过100M" })
                    // new FileTypeValidator({ fileType: MIMETypes.ALL_IMAGE })
                ]
            })
        )
        files: Express.Multer.File[],
        @Body() createFileDto: CreateFileDto,
        @Req() req: Request
    ) {
        // TODO：DTO验证失败，文件会照样上传成功。DTO验证和文件上传验证似乎是并行的，能否改成串行，在DTO验证成功后才允许上传？
        // 当前处理方式：虽然DTO验证失败，文件依旧上传成功，但可以在异常过滤器中捕获到文件上传接口的异常，因此一旦有异常就手动删除文件。
        // 问题: 虽然multer允许多文件上传，但是当前只会携带一个文件hash，而hash字段具有唯一约束，所以会导致上传成功，但是数据库文件表写入失败。
        // 因此：本接口限制成单文件上传。前端要实现多文件上传，可重复调用本接口。
        return AjaxResult.success(await this.fileService.createFilesRecord(req, createFileDto, files));
    }

    /**
     * 文件相关的默认响应头：
     *      'Content-Type': 'application/octet-stream',
     *      'Content-Disposition': 'attachment; filename="xxx.png"',
     * 浏览器处理文件的方式：
     *      1. 预览：设置文件对应的 Content-Type，并且设置 Content-Disposition: inline
     *      2. 下载：默认 Content-Type 是 octet-stream 表明是二进制数据，Disposition 是 attachment，所以浏览器默认是下载
     *
     * 注意：如果只设置 inline，不设置对应的 Content-Type，则浏览器对于二进制数据不知道怎么办，一般也是触发下载。
     */

    @ApiOperation({ summary: "流式响应文件", description: "默认为预览文件" })
    @ApiParam({
        name: "filename",
        type: String,
        example: "avatar-1725569555453_769441185-image_gif.png",
        description:
            "文件名，格式为：文件字段名-唯一标识符(时间戳_随机数)-&文件mime类型(主类型_子类型)&-文件5大分类类型.文件后缀"
    })
    @ApiQuery({ name: "disposition", enum: ["inline", "attachment"], required: false })
    @ApiHeader({ name: "range", required: false, description: "断点续传请求头" })
    @Get("/file/:filename")
    @SkipJwtAuth()
    @SkipThrottle() // 禁用限流
    getFile(
        @Param("filename") filename: string,
        @Query("disposition") disposition: "inline" | "attachment" = "inline",
        @Headers("range") range: string,
        @Res({ passthrough: true }) res: Response
    ): StreamableFile {
        const mimetype = filename.split("&")[1].split("_").join("/"); // 从文件名中提取 MIME 类型
        const category = filename.split(".")[0].split("-").pop(); // 从文件名中提取分类

        const diskStoragePath = this.configService.get(CONFIG_KEY.upload).dest;
        const fullPath = join(diskStoragePath, category, filename);

        if (!existsSync(fullPath)) throw new NotFoundException("File not found");

        // 同步获取文件元信息
        const fileStat = statSync(fullPath);
        if (!fileStat.isFile()) throw new NotFoundException("File not found");

        // res 响应对象直接设置响应头
        res.set({
            "Content-Type": mimetype || MIMETypes.BINARY, // 文件类型
            "Content-Disposition": `${disposition}; filename=${filename}`, // 下载时文件名
            "Content-Length": fileStat.size, // 文件总字节数
            "Cache-Control": "public, max-age=86400", // public：可以在任何地方被缓存，max-age：缓存时间(秒)。 缓存一天
            "Last-Modified": fileStat.mtime.toUTCString() // 文件最后修改时间。提供文件最后修改时间戳给客户端，以便于客户端判断缓存有效性
        });

        try {
            // 处理 Range 请求
            if (range) return this.handleRangeRequest(range, fullPath, res, fileStat.size);

            // 创建可读流
            const fileReadStream = createReadStream(fullPath);
            // 错误处理
            fileReadStream.on("error", err => {
                console.error("File stream error:", err);
                if (!res.headersSent) res.status(500).end();

                fileReadStream.destroy();
                // throw new InternalServerErrorException("File delivery failed");
            });

            // 客户端断开处理
            res.on("close", () => {
                if (!fileReadStream.destroyed) {
                    fileReadStream.destroy();
                    console.log("Client disconnected");
                }
            });

            // return new StreamableFile(fileReadStream, {
            //     type: mimetype || MIMETypes.BINARY,
            //     disposition: `${disposition}; filename=${filename}`,
            //     length: fileStat.size // 文件总字节数
            // });
            return new StreamableFile(fileReadStream);
        } catch (error) {
            console.error("File processing error:", error);
            throw new InternalServerErrorException("File delivery failed");
        }
    }

    /**
     * 处理 Range 请求头，返回相应的响应内容
     * @param range 请求头 Range 字段
     * @param fullPath 文件绝对路径
     * @param res 响应对象
     * @param fileSize 文件总字节数
     */
    private handleRangeRequest(range: string, fullPath: string, res: Response, fileSize: number) {
        if (!range) return;

        // 解析 Range 请求头，获取请求的块范围
        const rangeArr = range.replace(/bytes=/, "").split("-");
        const start = parseInt(rangeArr[0], 10);
        const end = rangeArr[1] ? parseInt(rangeArr[1], 10) : fileSize - 1;
        const chunkSize = end - start + 1;

        // 设置响应头：Range 响应
        res.set({
            "Content-Range": `bytes ${start}-${end}/${fileSize}`, // 响应内容范围
            "Content-Length": chunkSize, // 响应内容长度
            "Accept-Ranges": "bytes" // 响应内容范围单位
        });

        // 必须设置状态码为 206，告知客户端这只是内容的一部分，还需继续请求
        res.status(206);

        const fileReadStream = createReadStream(fullPath, { start, end });

        fileReadStream.on("error", err => {
            console.error("File stream error:", err);
            if (!res.headersSent) {
                res.status(500).end();
            }
            fileReadStream.destroy();
            // throw new InternalServerErrorException("File delivery failed");
        });

        // 客户端断开处理
        res.on("close", () => {
            if (!fileReadStream.destroyed) {
                fileReadStream.destroy();
                console.log("Client disconnected");
            }
        });

        return new StreamableFile(fileReadStream);
    }
}
